import { GUI } from 'dat.gui';
import * as Three from 'three';
import { Stats } from 'three-stats';

const {
  // MeshBasicMaterial: CommonMaterial
  // MeshLambertMaterial: CommonMaterial,
  MeshPhongMaterial: CommonMaterial,
} = Three;

interface IControls {
  rotationSpeed: number;
  bouncingSpeed: number;
}

interface IRenderContext {
  container: HTMLElement;
  scene: Three.Scene;
  camera: Three.PerspectiveCamera;
  step: number;
  controls: IControls;
  renderer?: Three.Renderer;
  stats?: Stats;
  plane?: Three.Mesh;
  cube?: Three.Mesh;
  sphere?: Three.Mesh;
}

function init(container: HTMLElement) {
  const scene = new Three.Scene();
  const camera = setupCamera(scene);
  const context: IRenderContext = {
    container,
    scene,
    camera,
    step: 0,
    controls: {
      rotationSpeed: 0.02,
      bouncingSpeed: 0.03,
    },
  };

  addUtilities(context);

  drawPlane(context);
  drawCube(context);
  drawSphere(context);

  addLight(context);

  setFog(context);

  handleAddCube(context);
  handleResize(context);

  render(context);
}

function setFog(context: IRenderContext) {
  // context.scene.fog = new Three.Fog(0xffffff, 0.015, 100);
  context.scene.fog = new Three.Fog(0xffffff, 15, 100);
  // context.scene.fog = new Three.FogExp2(0xffffff, 0.015);
}

function handleResize(context: IRenderContext) {
  window.addEventListener('resize', () => {
    const { width, height } = context.container.getBoundingClientRect();

    context.camera.aspect = width / height;
    context.camera.updateProjectionMatrix();

    context.renderer?.setSize(width, height);
  });
}

function handleAddCube(context: IRenderContext) {
  context.container.addEventListener('click', () => addCube(context));

  for (let i = 0; i < 100; i += 1) {
    addCube(context);
  }
}

function addCube(context: IRenderContext) {
  const cubeSize = Math.round(Math.random() * 3);

  const cubeGeometry = new Three.BoxGeometry(cubeSize, cubeSize, cubeSize);
  const cubeMaterial = new CommonMaterial({ color: 0xff0000 * Math.random(), wireframe: false });
  const cube = new Three.Mesh(cubeGeometry, cubeMaterial);

  const { width, height } = (context.plane?.geometry as Three.PlaneGeometry).parameters || {
    width: 10,
    height: 10,
  };

  cube.position.x = Math.round(-15 + Math.random() * width - cubeGeometry.parameters.width / 2);
  cube.position.y = Math.round(Math.random() * 5);
  cube.position.z = Math.round(-10 + Math.random() * height - cubeGeometry.parameters.height / 2);

  cube.castShadow = true;

  cube.name = `cube-${context.scene.children.length}`;

  context.scene.add(cube);
}

function addLight({ scene }: IRenderContext) {
  const ambientLight = new Three.AmbientLight(0x0c0c0c);
  scene.add(ambientLight);

  const spotLight = new Three.SpotLight(0xffffff);
  spotLight.position.set(-40, 60, -10);
  spotLight.castShadow = true;
  scene.add(spotLight);
}

function render(context: IRenderContext) {
  const { scene, camera, cube, sphere, controls, container } = context;
  const renderer = new Three.WebGLRenderer();

  // scene.overrideMaterial = new Three.MeshLambertMaterial({color: 0xffffff});

  const { width, height } = container.getBoundingClientRect();
  // renderer.setClearColor(0xeeeeee);
  renderer.setClearColor(new Three.Color(0xeeeeee));
  renderer.setSize(width, height);
  renderer.shadowMap.enabled = true;

  container.appendChild(renderer.domElement);
  context.renderer = renderer;

  // Start Animation
  const startAnimation = () => {
    context.stats?.update();

    if (cube) {
      cube.rotateX(controls.rotationSpeed);
      cube.rotateY(controls.rotationSpeed);
      cube.rotateZ(controls.rotationSpeed);
    }

    if (sphere) {
      context.step += controls.bouncingSpeed;
      Object.assign(sphere.position, {
        x: 20 + Math.cos(context.step) * 10,
        y: 2 + Math.sin(context.step) * 30,
      });
    }

    requestAnimationFrame(startAnimation);
    renderer.render(scene, camera);
  };

  startAnimation();
}

function setupCamera(scene: Three.Scene): Three.PerspectiveCamera {
  const camera = new Three.PerspectiveCamera();

  Object.assign(camera.position, {
    x: -30,
    y: 40,
    z: 30,
  });

  camera.lookAt(scene.position);
  return camera;
}

function drawSphere(context: IRenderContext) {
  const sphereGeometry = new Three.SphereGeometry(4, 20, 20);
  const sphereMaterial = new CommonMaterial({ color: 0x7777ff, wireframe: false });
  const sphere = new Three.Mesh(sphereGeometry, sphereMaterial);

  sphere.position.x = 20;
  sphere.position.y = 4;
  sphere.position.z = 2;

  sphere.castShadow = true;

  sphere.name = 'sphere';

  context.scene.add(sphere);
  context.sphere = sphere;
}

function addUtilities(context: IRenderContext) {
  addAxes(context);
  addStats(context);
  addControls(context);
}

function addControls(context: IRenderContext) {
  const gui = new GUI();
  gui.add(context.controls, 'rotationSpeed', 0, 0.5);
  gui.add(context.controls, 'bouncingSpeed', 0, 0.5);
}

function addStats(context: IRenderContext) {
  const container = document.getElementById('stats');

  if (container) {
    const stats = new Stats();
    stats.setMode(0);

    Object.assign(stats.domElement.style, {
      position: 'absolute',
      right: '0px',
      top: '0px',
      width: 'min-content',
    });

    container.appendChild(stats.domElement);

    context.stats = stats;
  }
}

function addAxes({ scene }: IRenderContext) {
  const axes = new Three.AxesHelper(20);
  scene.add(axes);
}

function drawPlane(context: IRenderContext) {
  const planeGeometry = new Three.PlaneGeometry(60, 20, 1, 1);
  const planeMaterial = new CommonMaterial({ color: 0xcccccc });
  const plane = new Three.Mesh(planeGeometry, planeMaterial);

  plane.rotateX(-0.5 * Math.PI);
  plane.position.x = 15;
  plane.position.y = 0;
  plane.position.z = 0;

  plane.receiveShadow = true;

  plane.name = 'plane';

  context.scene.add(plane);
  context.plane = plane;
}

function drawCube(context: IRenderContext) {
  const cubeGeometry = new Three.BoxGeometry(4, 4, 4);
  const cubeMaterial = new CommonMaterial({ color: 0xff0000, wireframe: false });
  const cube = new Three.Mesh(cubeGeometry, cubeMaterial);

  cube.position.x = -4;
  cube.position.y = 3;
  cube.position.z = 0;

  cube.castShadow = true;

  cube.name = 'cube-0';

  context.scene.add(cube);
  context.cube = cube;
}

function main() {
  // Error.stackTraceLimit = 100;
  const container = document.getElementById('app');

  if (container) {
    init(container);
  } else {
    console.error('找不到 #app ');
  }
}

// 入口
main();
